//    Copyright (c) 2009 Elwyn Malethan
//
//    This file is part of SeemoreJ.
//
//    SeemoreJ is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    SeemoreJ is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.

//    You should have received a copy of the GNU General Public License
//    along with SeemoreJ.  If not, see <http://www.gnu.org/licenses/>.

package com.malethan.seemorej;

import com.malethan.seemorej.context.ContextFactory;
import com.malethan.seemorej.context.Context;
import com.malethan.seemorej.context.impl.ContextFactoryImpl;
import com.malethan.seemorej.crud.StringUtils;
import com.malethan.seemorej.rendering.Rendering;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

public class ApplicationServlet extends HttpServlet {
    //----------------------------------------------------------------------- Static Properties and Constants

    private static final Log log = LogFactory.getLog(ApplicationServlet.class);

    private static final long serialVersionUID = 8507688075977861874L;

    //----------------------------------------------------------------------- Static Methods

    /**
     * Determines whether the currentAction should have a filter applied to it based on what actions should be included
     * or excluded. If o both arrays are empty the assumption is to apply the filter. If an action is both included and
     * excluded
     * @param include an array of actions to which the filter should be applied
     * @param exclude an array of actions to which the filter should NOT be applied
     * @param currentAction the currently invoked action
     * @return true if the currentAction is included and not excluded
     */
    protected static boolean isFilterAppliedTo(String[] include, String[] exclude, String currentAction) {
        boolean invoke = false;
        if(include.length == 0 && exclude.length == 0) {
            invoke = true;
        }
        else {
            if(include.length == 0 || Arrays.asList(include).contains(currentAction)) {
                invoke = true;
            }
            if(Arrays.asList(exclude).contains(currentAction)) {
                invoke = false;
            }
        }
        return invoke;
    }

    //----------------------------------------------------------------------- Instance Properties

    Object controller;
    ContextFactory contextFactory;

    //----------------------------------------------------------------------- Constructors
    //----------------------------------------------------------------------- Getters and Setters

    //----------------------------------------------------------------------- Instance Methods

    /**
     * This method should at the very least set contextFactory
     * @throws ServletException
     */
    @Override
    public void init() throws ServletException {
        super.init();

        log.info("init() invoked, setting default context factory");

        if(this.contextFactory == null) {
            this.contextFactory = new ContextFactoryImpl();
        }
    }

    @Override
    public final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        try {
            Context context = contextFactory.createContext(request, response);
            SeemoreJ.init(context);

            String controllerName = request.getParameter("sjController");
            String actionName = request.getParameter("sjAction");
            String format = request.getParameter(SeemoreJ.FORMAT);
            context.setController(controllerName);
            context.setAction(actionName);
            context.setFormat(format);

            try {
                controller = getControllerInstance(controllerName);
                Method action = getActionMethod(actionName, controller);
                String httpMethod = request.getMethod();
                try {
                    if (httpMethodAllowed(httpMethod, action)) {
                        invokeBeforeFilters(actionName);
                        action.invoke(controller);
                        invokeAfterFilters(actionName);
                    } else {
                        throw new ServletException("The action " + actionName + " cannot receive " + httpMethod + " requests");
                    }
                } catch (IllegalAccessException e) {
                    throw new ServletException("Action name '" + actionName + "' has incorrect access, must be public", e);
                } catch (InvocationTargetException e) {
                    throw new ServletException("An error occurred when invoking action '" + actionName + "'", e.getCause());
                }
            } catch (NoSuchActionException e) {
                log.debug("No such action '" + actionName + "' on controller '" + controllerName + "', assuming dumb view");
                invokeBeforeFilters(actionName);
                invokeAfterFilters(actionName);
            } catch (NoSuchControllerException e) {
                log.warn("Controller " + controllerName + " not found, assuming dumb view");
            }

            if(!context.isRendered()) {
                SeemoreJ.renderView(actionName);
            }

            Rendering rendering = context.getRendering();
            if(rendering != null) {
                rendering.render(getServletContext(), context);
            }
        } finally {
            SeemoreJ.free();
        }
    }

    private Method getActionMethod(String actionName, Object controller) throws NoSuchActionException {
        try {
            return controller.getClass().getMethod(actionName);
        } catch (NoSuchMethodException e) {
            try {
                //Try {action}Action instead in case it's called new or switch or something
                actionName += "Action";
                return controller.getClass().getMethod(actionName);
            } catch (NoSuchMethodException e1) {
                throw new NoSuchActionException(actionName);
            }
        }
    }

    private Object getControllerInstance(String controllerName) {
        String ctrlClassName = classify(controllerName);
        try {
            Class controllerClass = Class.forName(ctrlClassName);
            return controllerClass.newInstance();
        } catch (ClassNotFoundException e) {
            throw new NoSuchControllerException(ctrlClassName);
        } catch (Exception e) {
            throw new ErrorInitializingControllerException(ctrlClassName, e);
        }
    }

    private boolean httpMethodAllowed(String httpMethod, Method action) {
        boolean httpMethodAllowed = true;
        Accepts accepts = action.getAnnotation(Accepts.class);
        if (accepts != null) {
            httpMethodAllowed = false;
            for (int i = 0; i < accepts.method().length; i++) {
                HttpMethod allowedMethod = accepts.method()[i];
                if (httpMethod.equalsIgnoreCase(allowedMethod.getLiteral())) {
                    httpMethodAllowed = true;
                    break;
                }
            }
        }
        return httpMethodAllowed;
    }

    /**
     * Runs all before filters configured for a given action. Note: filter methods must be public
     * TODO: Perform some caching of the fliters to run for given controller/action combos
     * @param action The sction to run the before filters for
     */
    protected void invokeBeforeFilters(String action) throws ServletException {
        for (Method method : controller.getClass().getMethods()) {
            BeforeFilter filter = method.getAnnotation(BeforeFilter.class);
            if(filter != null) {
                boolean invoke = isFilterAppliedTo(filter.include(), filter.exclude(), action);
                if (invoke) {
                    invokeFilter(method);
                }
            }
        }
    }

    /**
     * Runs all before filters configured for a given action. Note: filter methods must be public
     * TODO: Perform some caching of the fliters to run for given controller/action combos
     * @param action The sction to run the before filters for
     */
    protected void invokeAfterFilters(String action) throws ServletException {
        for (Method method : controller.getClass().getMethods()) {
            AfterFilter filter = method.getAnnotation(AfterFilter.class);
            if(filter != null) {
                boolean invoke = isFilterAppliedTo(filter.include(), filter.exclude(), action);
                if (invoke) {
                    invokeFilter(method);
                }
            }
        }
    }

    protected void invokeFilter(Method method) throws ServletException {
        try {
            method.invoke(controller);
        } catch (InvocationTargetException e) {
            log.error("Filter '" + controller.getClass().getName() + "#" + method.getName() + "' threw an exception", e);
            throw new ServletException("Filter '" + controller.getClass().getName() + "#" + method.getName() + "' threw an exception", e);
        } catch (IllegalAccessException e) {
            //should never happen, method would not be in the collection unless it was public
        }
    }

    public String classify(String ctrlName) {
        return getPackagePrefix() + "."+ new StringUtils().capitaliseFirstLetter(ctrlName) + "Controller";
    }

    /**
     * packagePrefix is the package name, in which the controller classes can be found.
     * TODO: The use of the package prefix probably needs to change to make it more flexible
     * @return the name of the java package, in which controller classes can be found
     */
    protected String getPackagePrefix() {
        String pakage = getInitParameter("packagePrefix");
        if (pakage == null || pakage.length() == 0) {
            pakage = "app";
        }
        return pakage;
    }
}
